﻿#region Header
// ------------------------ Licence / Copyright ------------------------
// 
// ScrumTable for Scrum Meeting Support
// Copyright © HSR - Hochschule für Technik Rapperswil
// All Rights Reserved
// 
// Author:
//  Michael Gfeller Silvan Gehrig Patrick Boos
// 
// ---------------------------------------------------------------------
#endregion

#region Usings

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ScrumTable.Common;
using ScrumTable.Common.Collections;
using ScrumTable.Common.Data;
using ScrumTable.Common.Net;
using ScrumTable.Common.Security;
using ScrumTable.DL.Data.Common.Serialization;
using ScrumTable.DL.Data.Generic.Cache;
using ScrumTable.DL.Data.Local.Config.Driver;
using ScrumTable.DL.Data.Schema;
using ScrumTable.DL.Data.Schema.ScrumTableV1;

#endregion


namespace ScrumTable.DL.Data.Local
{
    /// <summary>
    /// Provides the facilities for deserialization of an xml file.
    /// </summary>
    internal class DataProvider
    {
        #region Declarations
        //--------------------------------------------------------------------
        // Declarations
        //--------------------------------------------------------------------

        private readonly IDictionary<int, ProjectData> _projectData = new Dictionary<int, ProjectData>();
        private readonly DriverConfig _config;
        private readonly CacheContext _context;
        private readonly ScrumTableV1Schema _schema;

        private bool _dataLoaded = false;
        private string _xmlFilePath;

        #endregion

        #region Properties
        //--------------------------------------------------------------------
        // Properties
        //--------------------------------------------------------------------

        /// <summary>
        /// Gets the loaded project instances.
        /// </summary>
        internal IList<CacheProject> Projects { get; private set; }

        /// <summary>
        /// Gets the deserialized data storage.
        /// </summary>
        private DataStorage Storage { get; set; }

        private ConnectionInformationManager ConnectionContext { get; set; }
        private DataObjectIndex<ICacheElement> Indices { get; set; }


        private ProjectData this[int cacheProjectIndex]
        {
            get
            {
                if (!_projectData.ContainsKey(cacheProjectIndex))
                    _projectData[cacheProjectIndex] = new ProjectData();
                return _projectData[cacheProjectIndex];
            }
        }

        private ProjectData this[ValueChain<string> rawDataObjectIds]
        {
            get { return this[Indices[new ValueChain<string>(rawDataObjectIds.Bottom)].Index]; }
        }

        #endregion

        #region Constructors / Destructor
        //--------------------------------------------------------------------
        // Constructors / Destructor
        //--------------------------------------------------------------------

        /// <summary>
        /// Creates a new DataProvider instance.
        /// </summary>
        /// <param name="config">Specifies the driver configuration settings.</param>
        /// <param name="context">Specifies the data context which contains the methods to create the child elements.</param>
        /// <param name="connectionCtx">Specifies the connection information context.</param>
        internal DataProvider(DriverConfig config, CacheContext context, ConnectionInformationManager connectionCtx)
        {
            PreCondition.AssertNotNull(config, "config");
            PreCondition.AssertNotNull(context, "context");
            PreCondition.AssertNotNull(connectionCtx, "connectionCtx");

            ConnectionContext = connectionCtx;
            _config = config;
            _context = context;
            _schema = context.Schema;
        }

        #endregion

        #region Methods
        //--------------------------------------------------------------------
        // Methods
        //--------------------------------------------------------------------

        /// <summary>
        /// Loads the project data blow the given project key chain.
        /// </summary>
        /// <param name="idChain">The data below the given id should be imported.</param>
        internal void LoadProjectData(ValueChain<string> idChain)
        {
            PreCondition.AssertNotNull(idChain, "idChain");

            foreach (CacheProject project in _context.Projects)
            {
                if (!_dataLoaded)
                {
                    // fill CacheProject with data
                    project.ImportData(
                        this[project.Id].Iterations,
                        this[project.Id].Reports,
                        this[project.Id].WorkItems.Convert(
                            indexEntry =>
                            new KeyValuePair<int, CacheWorkItem>(indexEntry.Index, (CacheWorkItem) indexEntry.DataLayerObject)));
                }

                if (project.UniqueId == idChain.Bottom)
                {
                    _context.CurrentProject = project;
                }
            }
            _dataLoaded = true;
        }

        /// <summary>
        /// Loads the projects from the given file.
        /// </summary>
        /// <param name="xmlFilePathToDeserialize">Specifies the path to the xml file to deserialize.</param>
        internal void LoadProjects(string xmlFilePathToDeserialize)
        {
            PreCondition.AssertNotNullOrEmpty(xmlFilePathToDeserialize, "xmlFilePathToDeserialize");

            _xmlFilePath = Path.GetDirectoryName(xmlFilePathToDeserialize); 
            
            Storage = new DataStorageSerializer(_schema).Deserialize(xmlFilePathToDeserialize);
            Indices = new DataObjectIndex<ICacheElement>(Storage);

            LoadTeams();
            LoadMembers();
            LoadProjects();

            LoadReports();
            LoadIterationData();
            LoadWorkItemData();
        }


        //--------------------------------------------------------------------
        // Load Projects Methods
        //--------------------------------------------------------------------

        private void LoadProjects()
        {
            Projects = new List<CacheProject>();

            foreach (DataObjectIndexEntry<ICacheElement> indexEntry in Indices.GetByDataAccessObject(_schema.ProjectSchema))
            {
                this[indexEntry.Index].ProjectName = GetData<string>(indexEntry.RawDataObject, _schema.ProjectSchema.Name, string.Empty);
                this[indexEntry.Index].ProjectBaseData = LoadBaseDataProvider(this[indexEntry.Index].Members);

                CacheProject newProject = _context.ElementFactory.CreateProject(
                    indexEntry.Index,
                    this[indexEntry.Index].ProjectName,
                    ServiceRegistration.Empty,
                    ServiceRegistration.Empty,
                    this[indexEntry.Index].Teams,
                    this[indexEntry.Index].Members,
                    this[indexEntry.Index].ProjectBaseData.WorkItemTypes);

                indexEntry.DataLayerObject = newProject;
                Projects.Add(newProject);
            }
        }

        private IBaseDataProvider LoadBaseDataProvider(IEnumerable<CacheMember> members)
        {
            object baseDataLoader = Activator.CreateInstance(
                Type.GetType(_config.BaseDataProvider, true, true),
                new object[] { _context, members });

            return (baseDataLoader as IBaseDataProvider);
        }

        private void LoadMembers()
        {
            foreach (DataObjectIndexEntry<ICacheElement> indexEntry in Indices.GetByDataAccessObject(_schema.MemberSchema))
            {
                string memberName = GetData(indexEntry.RawDataObject, _schema.MemberSchema.Name, string.Empty);
                ConnectionInformation connectionInfo = ConnectionContext.Where(ci => ci.OriginalUserName == memberName).FirstOrDefault();

                CacheMember newMember = _context.ElementFactory.CreateMember(
                    indexEntry.Index,
                    memberName,
                    GetData<string>(indexEntry.RawDataObject, _schema.MemberSchema.EMail, null),
                    string.Empty,
                    string.Empty,
                    memberName,
                    (connectionInfo != null),
                    LoadTeamRefs(indexEntry));

                if (connectionInfo != null)
                    connectionInfo.BackEndId = memberName;

                newMember.PictureUrl = GetUriData(indexEntry.RawDataObject, _schema.MemberSchema.PictureUri);

                indexEntry.DataLayerObject = newMember;
                this[indexEntry.RawDataObjectIds].Members.Add(newMember);
            }
        }

        private IEnumerable<string> LoadTeamRefs(DataObjectIndexEntry<ICacheElement> indexEntry)
        {
            ValueChain<string> teamChain = GetData(indexEntry.RawDataObject, _schema.MemberSchema.TeamIds, ValueChain<string>.Empty);
            IList<string> refs = new List<string>();

            if (teamChain != ValueChain<string>.Empty)
            {
                if (teamChain is MultiValueChain<string>)
                {
                    foreach (ValueChain<string> chain in ((MultiValueChain<string>) teamChain).MultiChain)
                    {
                        refs.Add(Indices[chain].DataLayerObject.ReferenceName);
                    }
                }
                else
                {
                    refs.Add(Indices[teamChain].DataLayerObject.ReferenceName);
                }
            }
            return refs;
        }

        private void LoadTeams()
        {
            foreach (DataObjectIndexEntry<ICacheElement> indexEntry in Indices.GetByDataAccessObject(_schema.TeamSchema))
            {
                string teamName = GetData(indexEntry.RawDataObject, _schema.TeamSchema.Name, string.Empty);

                CacheTeam newTeam = _context.ElementFactory.CreateTeam(
                    indexEntry.Index,
                    teamName,
                    teamName);

                indexEntry.DataLayerObject = newTeam;
                this[indexEntry.RawDataObjectIds].Teams.Add(newTeam);
            }
        }

        private void LoadReports()
        {
            foreach (DataObjectIndexEntry<ICacheElement> indexEntry in Indices.GetByDataAccessObject(_schema.ReportSchema))
            {
                NamedValue name = new NamedValue(
                    null,
                    GetData(indexEntry.RawDataObject, _schema.ReportSchema.Name, string.Empty));
                NamedValue path = new NamedValue(
                    null,
                    GetData(indexEntry.RawDataObject, _schema.ReportSchema.Path, string.Empty));
                Uri fullPath = GetUriData(indexEntry.RawDataObject, _schema.ReportSchema.FullPath);

                CacheFieldDefinition<NamedValue> nameField = _context.ElementFactory.CreateFieldDefinition(
                    _schema.ReportSchema.Name,
                    _schema.ReportSchema.Name.SchemaDisplayName,
                    DataType.String, 
                    name,
                    new[] { name });
                CacheFieldDefinition<NamedValue> pathField = _context.ElementFactory.CreateFieldDefinition(
                    _schema.ReportSchema.Path,
                    _schema.ReportSchema.Path.SchemaDisplayName,
                    DataType.String, 
                    path,
                    new[] { path });

                CacheReport newReport = _context.ElementFactory.CreateReport(
                    name.Value, path.Value, fullPath,
                    string.Empty, new[] { nameField, pathField });

                indexEntry.DataLayerObject = newReport;
                this[indexEntry.RawDataObjectIds].Reports.Add(newReport);
            }
        }


        //--------------------------------------------------------------------
        // Load Project Data
        //--------------------------------------------------------------------

        private void LoadWorkItemData()
        {
            LoadUserStories();
            LoadTasks();
            LoadBugs();
            UpdateReferences();
        }

        private void LoadBugs()
        {
            foreach (DataObjectIndexEntry<ICacheElement> indexEntry in Indices.GetByDataAccessObject(_schema.BugSchema))
            {
                CacheWorkItem newBug = _context.ElementFactory.CreateWorkItem(
                    this[indexEntry.RawDataObjectIds].ProjectBaseData.BugType,
                    indexEntry.Index,
                    GetData<string>(indexEntry.RawDataObject, _schema.BugSchema.Name, string.Empty),
                    0,
                    GetIterationId(indexEntry.RawDataObjectIds),
                    GetData<string>(indexEntry.RawDataObject, _schema.BugSchema.State, this[indexEntry.RawDataObjectIds].ProjectBaseData.BugType.Workflow.TransitionStart),
                    GetData<string>(indexEntry.RawDataObject, _schema.BugSchema.Description, null),
                    string.Empty, // this will be done through the UpdateReference() method
                    LoadValues(indexEntry.RawDataObject, _schema.BugSchema),
                    LoadParentLink(indexEntry.RawDataObjectIds),
                    new CacheWorkItemLink[0],
                    LoadAttachments(indexEntry.RawDataObjectIds),
                    new CacheWorkItemRevision[0]);

                indexEntry.DataLayerObject = newBug;
                this[indexEntry.RawDataObjectIds].WorkItems.Add(indexEntry);
            }
        }

        private void LoadUserStories()
        {
            foreach (DataObjectIndexEntry<ICacheElement> indexEntry in Indices.GetByDataAccessObject(_schema.UserStorySchema))
            {
                CacheWorkItem newUserStory = _context.ElementFactory.CreateWorkItem(
                    this[indexEntry.RawDataObjectIds].ProjectBaseData.UserStoryType,
                    indexEntry.Index,
                    GetData<string>(indexEntry.RawDataObject, _schema.UserStorySchema.Name, string.Empty),
                    0,
                    GetIterationId(indexEntry.RawDataObjectIds),
                    GetData<string>(indexEntry.RawDataObject, _schema.UserStorySchema.State, this[indexEntry.RawDataObjectIds].ProjectBaseData.UserStoryType.Workflow.TransitionStart),
                    GetData<string>(indexEntry.RawDataObject, _schema.UserStorySchema.Description, null),
                    string.Empty, // this will be done through the UpdateReference() method
                    LoadValues(indexEntry.RawDataObject, _schema.UserStorySchema),
                    new CacheWorkItemLink[0],
                    LoadChildLinks(indexEntry.RawDataObjectIds),
                    LoadAttachments(indexEntry.RawDataObjectIds),
                    new CacheWorkItemRevision[0]);

                indexEntry.DataLayerObject = newUserStory;
                this[indexEntry.RawDataObjectIds].WorkItems.Add(indexEntry);
            }
        }

        private void LoadTasks()
        {
            foreach (DataObjectIndexEntry<ICacheElement> indexEntry in Indices.GetByDataAccessObject(_schema.TaskSchema))
            {
                CacheWorkItem newTask = _context.ElementFactory.CreateWorkItem(
                    this[indexEntry.RawDataObjectIds].ProjectBaseData.TaskType,
                    indexEntry.Index,
                    GetData<string>(indexEntry.RawDataObject, _schema.TaskSchema.Name, string.Empty),
                    0,
                    GetIterationId(indexEntry.RawDataObjectIds),
                    GetData<string>(indexEntry.RawDataObject, _schema.TaskSchema.State, this[indexEntry.RawDataObjectIds].ProjectBaseData.TaskType.Workflow.TransitionStart),
                    GetData<string>(indexEntry.RawDataObject, _schema.TaskSchema.Description, null),
                    string.Empty, // this will be done through the UpdateReference() method
                    LoadValues(indexEntry.RawDataObject, _schema.TaskSchema),
                    LoadParentLink(indexEntry.RawDataObjectIds),
                    new CacheWorkItemLink[0],
                    LoadAttachments(indexEntry.RawDataObjectIds),
                    new CacheWorkItemRevision[0]);

                indexEntry.DataLayerObject = newTask;
                this[indexEntry.RawDataObjectIds].WorkItems.Add(indexEntry);
            }
        }

        private Dictionary<string, object> LoadValues(DataObject toGetValues, SchemaBase schema)
        {
            Dictionary<string, object> values = new Dictionary<string, object>();

            foreach (SchemaBase fieldSchema in schema.Schemata)
            {
                values[fieldSchema.SchemaMappedName] = GetData(toGetValues, fieldSchema);
            }
            return values;
        }

        private IList<CacheWorkItemLink> LoadChildLinks(ValueChain<string> rawParentChain)
        {
            // work item links are between UserStories [as Parent] and (Tasks or Bugs [as Children])
            IList<CacheWorkItemLink> links = new List<CacheWorkItemLink>();
            var children = from index in Indices.GetByDataAccessObjects(_schema.BugSchema, _schema.TaskSchema)
                           where index.RawDataObjectIds.ParentChain == rawParentChain
                           select index;
            children.ForEach(indexEntry => links.Add(_context.ElementFactory.CreateWorkItemLink(indexEntry.Index)));
            return links;
        }

        private IList<CacheWorkItemLink> LoadParentLink(ValueChain<string> childChain)
        {
            // work item links are between UserStories [as Parent] and (Tasks or Bugs [as Children])
            IList<CacheWorkItemLink> links = new List<CacheWorkItemLink>();
            var parent = (from index in Indices.GetByDataAccessObjects(_schema.UserStorySchema)
                         where index.RawDataObjectIds == childChain.ParentChain
                         select index).FirstOrDefault();
            if (parent != null)
                links.Add(_context.ElementFactory.CreateWorkItemLink(parent.Index));
            return links;
        }

        private IList<CacheAttachment> LoadAttachments(ValueChain<string> rawParentChain)
        {
            // attachments are below UserStories, Bugs and Tasks
            IList<CacheAttachment> attachments = new List<CacheAttachment>();
            var children = from index in Indices.GetByDataAccessObjects(_schema.AttachmentSchema)
                           where index.RawDataObjectIds.ParentChain == rawParentChain
                           select index;

            foreach (DataObjectIndexEntry<ICacheElement> indexEntry in children)
            {
                attachments.Add(
                    _context.ElementFactory.CreateAttachment(
                        GetData<string>(indexEntry.RawDataObject, _schema.AttachmentSchema.Name, null),
                        indexEntry.Index,
                        GetData<string>(indexEntry.RawDataObject, _schema.AttachmentSchema.Description, null),
                        GetUriData(indexEntry.RawDataObject, _schema.AttachmentSchema.Uri)));
            }
            return attachments;
        }

        private void UpdateReferences()
        {
            foreach (var projectPair in _projectData)
            {
                foreach (var workItemIndex in projectPair.Value.WorkItems)
                {
                    CacheWorkItem workItem = (CacheWorkItem)workItemIndex.DataLayerObject;

                    foreach (var fieldSchema in workItem.Type.Schema.SchemaReferences)
                    {
                        UpdateReference(workItemIndex.RawDataObject, workItem, fieldSchema);
                    }
                }
            }
        }

        private void UpdateReference(DataObject rawData, CacheWorkItem workItem, SchemaBase fieldSchema)
        {
            ValueChain<string> value = GetData<ValueChain<string>>(rawData, fieldSchema, null);

            if (!ValueChain<string>.IsNullOrEmpty(value))
            {
                /* value.ParentChains provides an Enumerable list as follows:
                 *  - ParentKey1
                 *  - ParentKey1 -> ParentKey2
                 *  - ParentKey1 -> ParentKey2 -> ParentKey3
                 *  
                 * each key is accommodated by the Index, which provides the
                 * data layer object whith the corresponding unique data layer
                 * id's.
                 */
                workItem[fieldSchema.SchemaMappedName] = Indices[value].DataLayerObject.ReferenceName;
            }
        }

        private void LoadIterationData()
        {
            foreach (DataObjectIndexEntry<ICacheElement> indexEntry in Indices.GetByDataAccessObject(_schema.IterationSchema))
            {
                CacheIteration newIteration = _context.ElementFactory.CreateIteration(
                    ObjectUtil.ConvertToString(indexEntry.Index),
                    indexEntry.Index,
                    indexEntry.Index,
                    GetData<string>(indexEntry.RawDataObject, _schema.IterationSchema.Name, string.Empty),
                    indexEntry.RawDataObject.Id,
                    GetData<DateTime>(indexEntry.RawDataObject, _schema.IterationSchema.StartTime, DateTime.Now),
                    GetData<DateTime>(indexEntry.RawDataObject, _schema.IterationSchema.EndTime, DateTime.Now) );

                indexEntry.DataLayerObject = newIteration;
                this[indexEntry.RawDataObjectIds].Iterations.Add(newIteration);
            }
        }

        //--------------------------------------------------------------------
        // Helper Methods
        //--------------------------------------------------------------------

        private int GetIterationId(ValueChain<string> elementChain)
        {
            if (elementChain.Count < 2)
                return CacheIdentity.DefaultId;

            DataObjectIndexEntry<ICacheElement> indexEntry = Indices[elementChain.SubChain(0, 2)];

            if (indexEntry.DataLayerObject is CacheIteration)
                return indexEntry.DataLayerObject.Id;
            return CacheIdentity.DefaultId;
        }

        private Uri GetUriData(DataObject toGetData, SchemaBase propSchema)
        {
            Uri loadedData = GetData<Uri>(toGetData, propSchema, null);

            if (loadedData != null && loadedData.ContainsFilePath())
            {
                /* if the given URL does not reference a file in the file system
                 * and there's a file relatively referenced to the Xml data file,
                 * us the relative path
                 */
                if (!File.Exists(loadedData.ToLocalPath())
                    && !loadedData.IsAbsoluteUri
                    && File.Exists(Path.Combine(_xmlFilePath, loadedData.ToLocalPath())))
                {
                    loadedData = new Uri(
                        Path.Combine(_xmlFilePath, loadedData.ToLocalPath()), 
                        UriKind.RelativeOrAbsolute);
                }
            }
            return loadedData;
        }

        private T GetData<T>(DataObject toGetData, SchemaBase propSchema, T defaultValue)
        {
            object retrievedData =
                (from dataValue in toGetData.DataValues
                 where dataValue.Name == propSchema.SchemaName
                 select dataValue.GetData(propSchema.SchemaType)).FirstOrDefault();

            if (retrievedData == null)
                return defaultValue;
            return (T) retrievedData;
        }

        private object GetData(DataObject toGetData, SchemaBase propSchema)
        {
            return (from dataValue in toGetData.DataValues
                    where dataValue.Name == propSchema.SchemaName
                    select dataValue.GetData(propSchema.SchemaType)).FirstOrDefault();
        }

        #endregion

        #region Events
        //--------------------------------------------------------------------
        // Events
        //--------------------------------------------------------------------

        #endregion
     

        #region Nested Classes
        //--------------------------------------------------------------------
        // Nested Classes
        //--------------------------------------------------------------------

        private class ProjectData
        {
            internal string ProjectName { get; set; }
            internal IBaseDataProvider ProjectBaseData { get; set; }

            internal IList<CacheTeam> Teams { get; private set; }
            internal IList<CacheMember> Members { get; private set; }
            internal IList<CacheReport> Reports { get; private set; }
            internal IList<CacheIteration> Iterations { get; private set; }
            internal IList<DataObjectIndexEntry<ICacheElement>> WorkItems { get; private set; }
            
            internal ProjectData()
            {
                Teams = new List<CacheTeam>();
                Members = new List<CacheMember>();
                Reports = new List<CacheReport>();
                Iterations = new List<CacheIteration>();
                WorkItems = new List<DataObjectIndexEntry<ICacheElement>>();
            }
        }
        
        #endregion
    }
}